package graphics.rasterizing;

import static util.MatrixOperationsF.add;
import static util.MatrixOperationsF.ar;
import static util.MatrixOperationsF.barryF;
import static util.MatrixOperationsF.multiply;
import static util.MatrixOperationsF.tocolor;
import graphics.rasterizing.camera.CameraPosition;
import graphics.rasterizing.model.Model;
import graphics.rasterizing.model.Triangle;
import graphics.rasterizing.projection.Projection;
import graphics.rasterizing.shading.Shader;

import java.util.Iterator;

import util.Transformations;

public class Rasterizer {
	private int _xmax;
	private int _ymax;
	private Model model;
	private float[][] zbuffer;
	private Shader _shader;
	private CameraPosition _camera;
	private Projection _projection;

	public Rasterizer(int xmax, int ymax) {
		_xmax = xmax;
		_ymax = ymax;
		zbuffer = new float[xmax][ymax];
		clearZBuffer();
	}

	private void clearZBuffer() {
		for (int i = 0; i < zbuffer.length; i++) {
			for (int j = 0; j < zbuffer[0].length; j++) {
				zbuffer[i][j] = Float.MAX_VALUE;
			}
		}
	}

	public void setProjection(Projection projection) {
		_projection = projection;
	}

	public void setCameraPosition(CameraPosition pos) {
		_camera = pos;
	}

	public void loadModel(Model model) {
		this.model = model;
	}

	public int[][] rasterize() {
		clearZBuffer();
		int[][] returnValue = initializeReturnValue();
		float[][] transformation = generateTransformations();
		Model trans = model.transform(transformation);
		for(Triangle t : trans.getTriangles())
			draw_on_zbuffer(t, returnValue);
		return returnValue;
	}

	private float[][] generateTransformations() {
		return multiply(Transformations.drawCanonical(_xmax, _ymax),
				multiply(_projection.getMatrix(), _camera.matrix()));
	}

	private int[][] initializeReturnValue() {
		int[][] returnValue = new int[_ymax][_xmax];
		for (int i = 0; i < returnValue.length; i++) {
			for (int j = 0; j < returnValue[0].length; j++) {
				returnValue[i][j] = tocolor(ar(0, 0, 0));
			}
		}
		return returnValue;
	}

	/**
	 * Draws the triangle on returnValue, note that the triangle must be
	 * completely in the returnvalue
	 * 
	 * @param transformedTriangle
	 * @param returnValue
	 */
	public void draw_on_zbuffer(Triangle transformedTriangle,
			int[][] returnValue) {
		int xmin = max(xmin(transformedTriangle), 0);
		int xmax = min(xmax(transformedTriangle), _xmax - 1);
		int ymin = max(ymin(transformedTriangle), 0);
		int ymax = min(ymax(transformedTriangle), _ymax - 1);
		float[] a = transformedTriangle.tri[0];
		float[] b = transformedTriangle.tri[1];
		float[] c = transformedTriangle.tri[2];
		float[][] abc = new float[3][];
		abc[0] = a;
		abc[1] = b;
		abc[2] = c;
		float Falpha = barryF(abc, 1, 2, a[0], a[1]);
		float FBeta = barryF(abc, 2, 0, b[0], b[1]);
		float FGamma = barryF(abc, 0, 1, c[0], c[1]);

		for (int x = xmin; x < xmax + 1; x++)
			for (int y = ymin; y < ymax + 1; y++) {

				float alpha = barryF(abc, 1, 2, x, y) / Falpha;
				float beta = barryF(abc, 2, 0, x, y) / FBeta;
				float gamma = barryF(abc, 0, 1, x, y) / FGamma;
				if (alpha > 0 && beta > 0 && gamma > 0) {
					float z = currentz(alpha, beta, gamma, abc);
					if (zbuffer[x][y] > z) {
						zbuffer[x][y] = z;
						returnValue[x][y] = tocolor(_shader.shade(alpha, beta,
								gamma, transformedTriangle));
					}

				}
			}
	}

	private float currentz(float alpha, float beta, float gamma, float[][] abc) {
		return add(multiply(abc[0], alpha), multiply(abc[1], beta),
				multiply(abc[2], gamma))[2];

	}

	private int min(int a, float b) {
		return (int) (a < b ? a : b);
	}

	private int max(int a, float b) {
		return (int) (a >= b ? a : b);
	}

	private int ymax(Triangle transformedTriangle) {
		int max = (int) transformedTriangle.tri[0][1];
		for (int i = 1; i < transformedTriangle.tri.length; i++) {
			max = max(max, transformedTriangle.tri[i][1]);
		}
		return max;
	}

	private int ymin(Triangle transformedTriangle) {

		int min = (int) transformedTriangle.tri[0][1];
		for (int i = 1; i < transformedTriangle.tri.length; i++) {
			min = min(min, transformedTriangle.tri[i][1]);
		}
		return min;
	}

	private int xmax(Triangle transformedTriangle) {
		int max = (int) transformedTriangle.tri[0][0];
		for (int i = 1; i < transformedTriangle.tri.length; i++) {
			max = max(max, transformedTriangle.tri[i][0]);
		}
		return max;
	}

	private int xmin(Triangle transformedTriangle) {
		int min = (int) transformedTriangle.tri[0][0];
		for (int i = 1; i < transformedTriangle.tri.length; i++) {
			min = min(min, transformedTriangle.tri[i][0]);
		}
		return min;
	}

	public void setShader(Shader shader) {
		this._shader = shader;
	}
}
